/*
* Copyright (C) 2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.chainfire.geolog.service;
import java.util.Locale;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.location.ActivityRecognitionClient;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import eu.chainfire.geolog.Debug;
import eu.chainfire.geolog.R;
import eu.chainfire.geolog.data.Database;
import eu.chainfire.geolog.data.Database.Accuracy;
import eu.chainfire.geolog.data.Database.Activity;
import eu.chainfire.geolog.data.Database.Profile.Type;
import eu.chainfire.geolog.ui.MainActivity;
import eu.chainfire.geolog.ui.SettingsFragment;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
public class BackgroundService extends Service {
public static void startService(Context context) {
context.startService(new Intent(context.getApplicationContext(), BackgroundService.class));
}
private static String EXTRA_ALARM_CALLBACK = "eu.chainfire.geolog.EXTRA.ALARM_CALLBACK";
private volatile ServiceThread thread = null;
private volatile PowerManager.WakeLock wakelock = null;
private volatile NotificationManager notificationManager;
private volatile PendingIntent notificationIntent;
private volatile Notification.Builder notificationBuilder;
@SuppressLint("NewApi")
@Override
public void onCreate() {
super.onCreate();
Debug.log("Service created");
if (thread == null) {
Debug.log("Launching thread");
thread = new ServiceThread();
thread.setContext(getApplicationContext());
thread.start();
}
PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE);
wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GeoLog Wakelock");
Intent i = new Intent();
i.setAction(Intent.ACTION_MAIN);
i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setClass(this, MainActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_NEW_TASK);
notificationIntent = PendingIntent.getActivity(this, 0, i, 0);
notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
notificationBuilder = (new Notification.Builder(this)).
setSmallIcon(R.drawable.ic_stat_service).
setContentIntent(notificationIntent).
setWhen(System.currentTimeMillis()).
setAutoCancel(false).
setOngoing(true).
setContentTitle(getString(R.string.service_title)).
setContentText(getString(R.string.service_waiting));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
notificationBuilder.setShowWhen(false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
/* quick turn off, maybe ? if added, make sure to add a button to preferences to disable these buttons
notificationBuilder.
setPriority(Notification.PRIORITY_MAX).
addAction(0, "A", notificationIntent).
addAction(0, "B", notificationIntent).
addAction(0, "C", notificationIntent);
*/
}
updateNotification();
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private void updateNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
notificationManager.notify(1, notificationBuilder.build());
} else {
notificationManager.notify(1, notificationBuilder.getNotification());
}
}
@Override
public void onDestroy() {
Debug.log("Stopping thread");
thread.signalStop();
try { thread.join(); } catch (Exception e) { }
thread = null;
((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).cancelAll();
Debug.log("Service destroyed");
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
thread.processIntent(intent);
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class ServiceThread extends Thread {
private static final int FLAG_SETUP = 1;
private static final int FLAG_ACTIVITY_UPDATE = 2;
private static final int FLAG_LOCATION_UPDATE = 4;
private static final int FLAG_PROFILE = 8;
private volatile Context context = null;
private volatile Handler handler = null;
private volatile AlarmManager alarm = null;
private volatile PendingIntent alarmCallback = null;
private volatile ActivityRecognitionClient activityClient = null;
private volatile boolean activityConnected = false;
private volatile PendingIntent activityIntent = null;
private volatile LocationClient locationClient = null;
private volatile boolean locationConnected = false;
private volatile Database.Helper databaseHelper = null;
private volatile boolean metric = true;
private volatile Database.Activity lastActivity = Database.Activity.UNKNOWN;
private volatile int lastConfidence = 0;
private volatile Location lastLocLoc = null;
private volatile Database.Location lastLocation = null;
private volatile long lastLocationDuplicates = 0;
private volatile long lastNonUnknown = 0;
private volatile Database.Accuracy lastLocationAccuracy = Database.Accuracy.NONE;
private volatile int lastLocationInterval = -1;
private volatile int lastActivityInterval = -1;
private volatile int lastBatteryLevel = 0;
private volatile boolean isSegmentStart = true;
private volatile long lastProfileUpdate = SystemClock.elapsedRealtime();
private volatile long scheduledReduceAccuracyTime = 0;
private volatile SharedPreferences prefs = null;
private volatile Database.Profile currentProfile = null;
// Main thread
public void setContext(Context context) {
this.context = context;
}
public void signalStop() {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
Looper.myLooper().quit();
}
});
}
}
public boolean processIntent(Intent intent) {
if (intent == null) return false;
if (ActivityRecognitionResult.hasResult(intent)) {
processActivity(intent);
return true;
} else if (intent.hasExtra(EXTRA_ALARM_CALLBACK)) {
processAlarmCallback(intent);
return true;
}
return false;
}
private void processActivity(Intent intent) {
if (handler != null) {
final DetectedActivity activity = ActivityRecognitionResult.extractResult(intent).getMostProbableActivity();
if (Database.isDetectedActivityValid(activity)) {
wakelock.acquire();
handler.post(new Runnable() {
@Override
public void run() {
setActivity(Database.activityFromDetectedActivity(activity), activity.getConfidence());
wakelock.release();
}
});
}
}
}
private void processAlarmCallback(Intent intent) {
if (handler != null) {
wakelock.acquire();
handler.post(new Runnable() {
@Override
public void run() {
updateListeners(0);
wakelock.release();
}
});
}
}
// Service thread
private void setActivity(Database.Activity activity, int confidence) {
if ((activity == Activity.UNKNOWN) && (SystemClock.elapsedRealtime() < lastNonUnknown + (2 * 60 * 1000)) && (SystemClock.elapsedRealtime() > lastNonUnknown)) {
return;
}
if (activity != Activity.UNKNOWN) {
lastNonUnknown = SystemClock.elapsedRealtime();
}
Debug.log(String.format(Locale.ENGLISH, "A: %s (%d%%)", Database.activityToString(activity), confidence));
lastActivity = activity;
lastConfidence = confidence;
updateListeners(FLAG_ACTIVITY_UPDATE);
}
private void setLocation(Location location) {
Debug.log(String.format(Locale.ENGLISH, "L: lat=%.8f long=%.5f alt=%.5f bearing=%.4f speed=%.4f accuracy=%.2f", location.getLatitude(), location.getLongitude(), location.getAltitude(), location.getBearing(), location.getSpeed(), location.getAccuracy()));
lastLocLoc = location;
updateListeners(FLAG_LOCATION_UPDATE);
}
@SuppressLint("NewApi")
private void updateListeners(int flags) {
if (!(activityConnected && locationConnected && (currentProfile != null))) return;
if (currentProfile.getType() == Type.OFF) {
stopSelf();
}
if ((flags & FLAG_PROFILE) == FLAG_PROFILE) {
Debug.log("Profile update");
lastActivity = Activity.UNKNOWN;
lastConfidence = 0;
scheduledReduceAccuracyTime = 0L;
lastProfileUpdate = SystemClock.elapsedRealtime();
}
Accuracy originalAccuracy = lastLocationAccuracy;
Database.Profile.ActivitySettings wanted = currentProfile.getActivitySettings(lastActivity);
Database.Accuracy wantedAccuracy = wanted.getAccuracy();
int wantedLocationInterval = wanted.getLocationInterval();
int wantedActivityInterval = wanted.getActivityInterval();
boolean allowUpdateActivityInterval = true;
boolean allowUpdateLocationInterval = true;
boolean allowUpdateLocationAccuracy = true;
if (
(
(SystemClock.elapsedRealtime() > lastProfileUpdate + (90 * 1000)) ||
(SystemClock.elapsedRealtime() < lastProfileUpdate)
) &&
(currentProfile.getReduceAccuracyDelay() > 0) &&
(
(wantedActivityInterval > lastActivityInterval) ||
(wantedLocationInterval > lastLocationInterval) ||
(Database.accuracyToInt(wantedAccuracy) < Database.accuracyToInt(lastLocationAccuracy))
)
) {
long left = 0;
if (scheduledReduceAccuracyTime == 0) {
left = currentProfile.getReduceAccuracyDelay() * 1000;
scheduledReduceAccuracyTime = SystemClock.elapsedRealtime() + left;
alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, scheduledReduceAccuracyTime + 1000, alarmCallback);
} else {
left = scheduledReduceAccuracyTime - SystemClock.elapsedRealtime();
}
if (left <= 0) scheduledReduceAccuracyTime = 0L;
allowUpdateActivityInterval = ((left <= 0) || (wantedActivityInterval < lastActivityInterval) || (lastActivityInterval == -1));
allowUpdateLocationInterval = ((left <= 0) || (wantedLocationInterval < lastLocationInterval) || (lastLocationInterval == -1));
allowUpdateLocationAccuracy = ((left <= 0) || (Database.accuracyToInt(wantedAccuracy) > Database.accuracyToInt(lastLocationAccuracy)));
if (!allowUpdateActivityInterval) wantedActivityInterval = lastActivityInterval;
if (!allowUpdateLocationInterval) wantedLocationInterval = lastLocationInterval;
if (!allowUpdateLocationAccuracy) wantedAccuracy = lastLocationAccuracy;
if (!allowUpdateActivityInterval) Debug.log(String.format(Locale.ENGLISH, "ActivityInterval --> Delay (%ds remaining)", (left / 1000)));
if (!allowUpdateLocationInterval) Debug.log(String.format(Locale.ENGLISH, "LocationInterval --> Delay (%ds remaining)", (left / 1000)));
if (!allowUpdateLocationAccuracy) Debug.log(String.format(Locale.ENGLISH, "LocationAccuracy --> Delay (%ds remaining)", (left / 1000)));
} else {
scheduledReduceAccuracyTime = 0;
alarm.cancel(alarmCallback);
}
if ((wantedAccuracy != lastLocationAccuracy) || (wantedLocationInterval != lastLocationInterval)) {
String s = "NONE";
if (wantedAccuracy == Accuracy.LOW) s = "LOW";
if (wantedAccuracy == Accuracy.HIGH) s = "HIGH";
Debug.log(String.format(Locale.ENGLISH, "Location --> %s %ds", s, wantedLocationInterval));
locationClient.removeLocationUpdates(locationListener);
if ((wantedAccuracy != Accuracy.NONE) && (wantedLocationInterval > 0)) {
if ((lastLocationAccuracy == Accuracy.NONE) || (lastLocationInterval == 0)) isSegmentStart = true;
LocationRequest req = new LocationRequest();
req.setFastestInterval(wantedLocationInterval * 250);
req.setInterval(wantedLocationInterval * 1000);
if (lastLocationAccuracy == Accuracy.NONE) req.setPriority(LocationRequest.PRIORITY_NO_POWER);
if (lastLocationAccuracy == Accuracy.LOW) req.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
if (lastLocationAccuracy == Accuracy.HIGH) req.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
locationClient.requestLocationUpdates(req, locationListener);
}
lastLocationAccuracy = wantedAccuracy;
lastLocationInterval = wantedLocationInterval;
}
if (wantedActivityInterval != lastActivityInterval) {
Debug.log(String.format(Locale.ENGLISH, "Activity --> %ds", wantedActivityInterval));
activityClient.removeActivityUpdates(activityIntent);
if ((wantedActivityInterval == 0) && (lastActivityInterval != 0)) {
lastActivity = Activity.UNKNOWN;
lastConfidence = 0;
}
if (wantedActivityInterval != 0) {
activityClient.requestActivityUpdates(wantedActivityInterval * 1000, activityIntent);
}
lastActivityInterval = wantedActivityInterval;
}
if ((flags & FLAG_ACTIVITY_UPDATE) == FLAG_ACTIVITY_UPDATE) {
if (
(lastLocation == null) ||
(lastLocation.getActivity() != lastActivity) ||
(lastLocation.getConfidence() != lastConfidence)
) {
Debug.log("Activity update");
if (lastLocation == null) {
lastLocationDuplicates = 0;
lastLocation = new Database.Location();
lastLocation.setTime(System.currentTimeMillis());
}
lastLocation.setActivity(lastActivity);
lastLocation.setConfidence(lastConfidence);
}
} else if ((flags & FLAG_LOCATION_UPDATE) == FLAG_LOCATION_UPDATE) {
if (
(lastLocation == null) ||
(lastLocLoc == null) ||
(lastLocation.getLatitude() != lastLocLoc.getLatitude()) ||
(lastLocation.getLongitude() != lastLocLoc.getLongitude()) ||
(lastLocation.getAccuracyDistance() > lastLocLoc.getAccuracy()) ||
(lastLocation.getActivity() != lastActivity) ||
(lastLocation.getConfidence() != lastConfidence) ||
(lastLocation.getAccuracySetting() != originalAccuracy) ||
(isSegmentStart)
) {
Debug.log("Location update");
if (lastLocationDuplicates > 0) {
Debug.log("Saving last duplicate (out of " + String.valueOf(lastLocationDuplicates) + ")");
Database.Location.copy(databaseHelper, lastLocation);
}
lastLocationDuplicates = 0;
Database.Location loc = new Database.Location();
loc.setActivity(lastActivity);
loc.setConfidence(lastConfidence);
loc.setBattery(lastBatteryLevel);
loc.setAccuracySetting(originalAccuracy);
loc.isSegmentStart(isSegmentStart);
loc.loadFromLocation(lastLocLoc);
Debug.log("Saved to database: " + String.valueOf(loc.saveToDatabase(databaseHelper)));
lastLocation = loc;
isSegmentStart = false;
} else if (
(lastLocation != null) &&
(lastLocLoc != null)
) {
lastLocationDuplicates++;
lastLocation.setActivity(lastActivity);
lastLocation.setConfidence(lastConfidence);
lastLocation.setBattery(lastBatteryLevel);
lastLocation.setAccuracySetting(originalAccuracy);
lastLocation.isSegmentStart(isSegmentStart);
lastLocation.loadFromLocation(lastLocLoc);
}
}
if (lastLocation != null) {
notificationBuilder.
setWhen(lastLocation.getTime()).
setContentText(String.format(Locale.ENGLISH, "%s ~ %d%% / %.5f, %.5f ~ %.0f%s", Database.activityToString(lastLocation.getActivity()), lastLocation.getConfidence(), lastLocation.getLatitude(), lastLocation.getLongitude(), metric ? lastLocation.getAccuracyDistance() : lastLocation.getAccuracyDistance() * SettingsFragment.METER_FEET_RATIO, metric ? "m" : "ft"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
notificationBuilder.setShowWhen(true);
}
updateNotification();
}
}
private ConnectionCallbacks activityConnectionCallbacks = new ConnectionCallbacks() {
@Override
public void onConnected(Bundle arg0) {
Debug.log("ActivityRecognitionClient connected");
activityConnected = true;
updateListeners(FLAG_SETUP);
}
@Override
public void onDisconnected() {
Debug.log("ActivityRecognitionClient disconnected");
activityConnected = false;
}
};
private OnConnectionFailedListener activityConnectionFailed = new OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult arg0) {
Debug.log("ActivityRecognitionClient connection failed");
activityConnected = false;
signalStop();
}
};
private LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location arg0) {
wakelock.acquire();
try {
setLocation(arg0);
} finally {
wakelock.release();
}
}
};
private ConnectionCallbacks locationConnectionCallbacks = new ConnectionCallbacks() {
@Override
public void onConnected(Bundle arg0) {
Debug.log("LocationClient connected");
locationConnected = true;
updateListeners(FLAG_SETUP);
}
@Override
public void onDisconnected() {
Debug.log("LocationClient disconnected");
locationConnected = false;
}
};
private OnConnectionFailedListener locationConnectionFailed = new OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult arg0) {
Debug.log("LocationClient connection failed");
locationConnected = false;
signalStop();
}
};
private BroadcastReceiver databaseUpdated = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (
(currentProfile != null) &&
(intent != null) &&
intent.hasExtra(Database.Helper.EXTRA_TABLE) &&
intent.getStringExtra(Database.Helper.EXTRA_TABLE).equals(Database.Profile.TABLE_NAME) &&
intent.hasExtra(Database.Helper.EXTRA_ID) &&
(intent.getLongExtra(Database.Helper.EXTRA_ID, 0) == currentProfile.getId())
) {
currentProfile = Database.Profile.getById(databaseHelper, intent.getLongExtra(Database.Helper.EXTRA_ID, 0), currentProfile);
updateListeners(FLAG_PROFILE);
}
}
};
private OnSharedPreferenceChangeListener preferencesUpdated = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(SettingsFragment.PREF_UNITS)) {
metric = !prefs.getString(SettingsFragment.PREF_UNITS, SettingsFragment.PREF_UNITS_DEFAULT).equals(SettingsFragment.VALUE_UNITS_IMPERIAL);
updateListeners(0);
}
if (key.equals(SettingsFragment.PREF_CURRENT_PROFILE)) {
currentProfile = Database.Profile.getById(databaseHelper, sharedPreferences.getLong(key, 0), currentProfile);
updateListeners(FLAG_PROFILE);
}
}
};
private BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean plugged = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
boolean charging = (
(status == BatteryManager.BATTERY_STATUS_CHARGING) ||
((status == BatteryManager.BATTERY_STATUS_FULL) && plugged)
);
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
if (charging) level += 100;
lastBatteryLevel = level;
}
};
@Override
public void run() {
Debug.log("Thread init");
databaseHelper = Database.Helper.getInstance(context);
alarm = (AlarmManager)context.getSystemService(ALARM_SERVICE);
{
Intent i = new Intent(context.getApplicationContext(), BackgroundService.class);
i.putExtra(EXTRA_ALARM_CALLBACK, 1);
alarmCallback = PendingIntent.getService(BackgroundService.this, 0, i, 0);
}
Looper.prepare();
handler = new Handler();
Debug.log("Registering for updates");
prefs = PreferenceManager.getDefaultSharedPreferences(context);
long id = prefs.getLong(SettingsFragment.PREF_CURRENT_PROFILE, 0);
if (id > 0) currentProfile = Database.Profile.getById(databaseHelper, id, null);
if (currentProfile == null) currentProfile = Database.Profile.getOffProfile(databaseHelper);
metric = !prefs.getString(SettingsFragment.PREF_UNITS, SettingsFragment.PREF_UNITS_DEFAULT).equals(SettingsFragment.VALUE_UNITS_IMPERIAL);
prefs.registerOnSharedPreferenceChangeListener(preferencesUpdated);
LocalBroadcastManager.getInstance(context).registerReceiver(databaseUpdated, new IntentFilter(Database.Helper.NOTIFY_BROADCAST));
Debug.log("Registering for power levels");
context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
Debug.log("Connecting ActivityRecognitionClient");
activityIntent = PendingIntent.getService(context, 1, new Intent(context, BackgroundService.class), 0);
activityClient = new ActivityRecognitionClient(context, activityConnectionCallbacks, activityConnectionFailed);
activityClient.connect();
Debug.log("Connecting LocationClient");
locationClient = new LocationClient(context, locationConnectionCallbacks, locationConnectionFailed);
locationClient.connect();
Debug.log("Entering loop");
handler.post(new Runnable() {
@Override
public void run() {
updateListeners(FLAG_SETUP);
}
});
Looper.loop();
Debug.log("Exiting loop");
context.unregisterReceiver(batteryReceiver);
LocalBroadcastManager.getInstance(context).unregisterReceiver(databaseUpdated);
prefs.unregisterOnSharedPreferenceChangeListener(preferencesUpdated);
if (activityConnected) {
activityClient.removeActivityUpdates(activityIntent);
activityClient.disconnect();
}
if (locationConnected) {
locationClient.removeLocationUpdates(locationListener);
locationClient.disconnect();
}
}
}
}